import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;

public class ServerEngine extends Thread {

	public final Vector<User> users = new Vector<User>();
	public final Vector<Game> games = new Vector<Game>();
	
	private Selector selector;
    private ServerSocketChannel serverSocket;
    
	public class UserAcceptor {

	    public UserAcceptor() {}

	    public void accept() throws IOException {
	        final SocketChannel clientChannel = serverSocket.accept();

	        if (clientChannel != null) {
	            clientChannel.configureBlocking(false);
	            SelectionKey selectionKey = clientChannel.register(selector, SelectionKey.OP_READ);
	            
	            User user;
	            users.add(user = new User(selectionKey, clientChannel));
	            selectionKey.attach(new UserReader(user));
	        }
	    }
	}
	
	public class UserReader {

	    private CharsetDecoder decoder = Charset.forName("Windows-1251").newDecoder();
	    private ByteBuffer inputBuffer = ByteBuffer.allocate(256);
	    private StringBuffer stringBuffer = new StringBuffer();
	    private User user;

	    public UserReader(User us) {
	    	user = us;
	    }

	    public void read() throws IOException {
	        inputBuffer.clear();
	        int readBytes = 0;
			try {
				readBytes = user.getSocketChannel().read(inputBuffer);
			} catch (IOException e) {
				closeSocket();
			}

	        if (readBytes < 0) {
	            closeSocket();
	        } else if (readBytes > 0) {

	            inputBuffer.flip();
	            stringBuffer.append(new String(inputBuffer.array(), 0, readBytes));

	            if (isReadComplete(inputBuffer)) {
	            	proc(stringBuffer.toString());
	                inputBuffer.clear();
	                stringBuffer.delete(0, stringBuffer.length());
	            }
	        }
	    }
	    
		public void proc(String in) throws IOException {

			StringTokenizer st = new StringTokenizer(in);
			
			if (st.hasMoreTokens()) {
				String command = st.nextToken().trim();
				
				if (command.equals("lst")) {
					StringBuffer res = new StringBuffer();
					res.append(" ");
		            int count = 0;
		            for (Game game : games) {
		            	if (game.getOpponent() == null) {
		            		++count;
		            		res.append(game.hashCode() + " " + game.getInfo() + " " + game.getX() + " " + game.getY() + 
		            				" " + game.getK() + " " + game.getStart() + " ");
		            	}
		            }
		            user.send("lst " + count + res.toString());
		            return;
				} else if (command.equals("ng")) {
					
					try {
						String info = st.nextToken();
						
						int x = Integer.parseInt(st.nextToken());
						int y = Integer.parseInt(st.nextToken());
						int k = Integer.parseInt(st.nextToken());
						int start = Integer.parseInt(st.nextToken());
						
						//if (!user.isGameCreated()) {
							Game game = new Game(user, info, x, y, k, start);
							games.add(game);
						//	user.setGameCreated(true);
							user.send("ok "+game.hashCode());
						//} else {
						//	user.send("fail");
						//}
					} catch (NumberFormatException e) {
						e.printStackTrace();
						user.send("error Command 'ng': syntax error (NumberFormatException)");
					} catch (NoSuchElementException e) {
						e.printStackTrace();
						user.send("error Command 'ng': syntax error (NoSuchElementException)");
					}
					return;
				} else if (command.equals("conn")) {
					
					if (!st.hasMoreTokens()) {
						user.send("error Command 'conn': syntax error (NoSuchElementException)");
						return;
					}
					
					String hash = st.nextToken();
					
					for (Game g : games) {
						if (hash.equals(g.hashCode() + "") && g.getOpponent() == null) {
							
							g.start(user);
							
							ServerFrame.getInstance().addGame(g);
							
							if (g.getNextMove().equals(g.getOwner())) {
								g.getOwner().send("gi " + g.hashCode() + " x");
								g.getOpponent().send("gi " + g.hashCode() + " o");
							} else {
								g.getOwner().send("gi " + g.hashCode() + " o");
								g.getOpponent().send("gi " + g.hashCode() + " x");
							}
							return;
						}
					}
					user.send("fail");
					return;
				} else if (command.equals("mv")) {
					
					try {
						String hash = st.nextToken();
						
						int x = Integer.parseInt(st.nextToken());
						int y = Integer.parseInt(st.nextToken());
						// TODO fix it, if only one game supported - user.game
						for (Game g : games) {
							if (hash.equals(g.hashCode() + "")) {
								g.move(user, x, y);
								return;
							}
						}
						user.send("error Command 'mv': game#" + hash + " not exist");
					} catch (NumberFormatException e) {
						user.send("error Command 'mv': syntax error (NumberFormatException)");
					} catch (NoSuchElementException e) {
						user.send("error Command 'mv': syntax error (NoSuchElementException)");
					}
					return;
				} else if (command.equals("cancel")) {
					if (!st.hasMoreTokens()) {
						user.send("error Command 'cancel': syntax error (NoSuchElementException)");
						return;
					}
						
					String hash = st.nextToken();
					// TODO fix it, if only one game supported - user.game
					for (Game g : games) {
						if (hash.equals(g.hashCode() + "")) {
							g.getOwner().send("cancel "+hash);
							g.getOpponent().send("cancel "+hash);
							g.remove();
							break;
						}
					}
					return;
				}
			}
		}

	    private boolean isReadComplete(ByteBuffer buf) {
	        return byteBufferToString(buf).endsWith("\n");
	    }

	    private String byteBufferToString(ByteBuffer buf) {
	        buf.mark();

	        CharBuffer charBuf = CharBuffer.allocate(256);
	        charBuf.clear();
	        decoder.decode(buf, charBuf, false);
	        charBuf.flip();

	        buf.reset();
	        return charBuf.toString();
	    }

	    private void closeSocket() {
	    	try {
		        users.remove(user);

				Vector<Game> toDel = new Vector<Game>();
				for (Game g : games) {
					if (g.getOwner().equals(user)) {
						if (g.getOpponent() != null)
							g.getOpponent().send("cancel "+g.hashCode());
						toDel.add(g);
						continue;
					}
					if (g.getOpponent() != null)
						if (g.getOpponent().equals(user)) {
							g.getOwner().send("cancel "+g.hashCode());
							toDel.add(g);
							continue;
						}
				}
				for (Game g : toDel) {
					g.remove();
				}
				
		        if (user.getSocketChannel().isConnected()) {
		        	user.getSocketChannel().close();
		        }
				user.getSelectionKey().cancel();
			} catch (IOException e) {
                e.printStackTrace();
            }
	    }
	}

	private static class SingletonHolder {
		public static ServerEngine instance = new ServerEngine();
	}
	
	public static ServerEngine getInstance() {
		return SingletonHolder.instance;
	}
	
    private ServerEngine() {}

	public void init(int port) {
    	try {
			selector = Selector.open();

			serverSocket = ServerSocketChannel.open();
			serverSocket.socket().bind(new InetSocketAddress("localhost", port));
			serverSocket.configureBlocking(false);

			serverSocket.register(selector, SelectionKey.OP_ACCEPT, new UserAcceptor());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
    
    public void run() {
    	if (serverSocket == null)
    		throw new NullPointerException();
        while (!Thread.interrupted() && selector.isOpen()) {
            try {
                selector.select();

                Set<SelectionKey> selected = selector.selectedKeys();
                Iterator<SelectionKey> it = selected.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();

                    if (key.isValid() && key.isAcceptable()) {
                        UserAcceptor acceptor = (UserAcceptor) key.attachment();
                        acceptor.accept();
                    } else if (key.isValid() && key.isReadable()) {
                        UserReader reader = (UserReader) key.attachment();
                        try {
                        	reader.read();
                        } catch(IOException e) {
                        	reader.closeSocket();
                        }
                    }
                }
                selected.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void disconnectAll() {
    	for (User u : users) {
    		try {
    			u.send("disconnect");
    			u.getSelectionKey().cancel();
    			u.getSocketChannel().close();
    		} catch (IOException e) {
				e.printStackTrace();
			}
    	}
    	games.clear();
    	users.clear();
    }

	public Vector<Game> getGames() {
		return games;
	}
	
	public void removeGame(Game g) {
		games.remove(g);
	}
	
}
